﻿using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.IO;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Windows;
using System.Text.RegularExpressions;

namespace SQ816FWUpdater
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        #region INotifyPropertyChanged from my standard pile of goop
        public event PropertyChangedEventHandler PropertyChanged;
        #region OnPropertyChanged method group

        #region GetPropertyExpr helper
        private static string GetPropertyExpr(Expression<Func<object>> accessor)
        {
            MemberExpression me = accessor.Body as MemberExpression;
            if (me == null)
            {
                UnaryExpression ue = accessor.Body as UnaryExpression;
                if (ue == null)
                    throw new NotSupportedException("Invalid expression");
                me = ue.Operand as MemberExpression;
                if (me == null)
                    throw new NotSupportedException("Invalid expression");
            }
            return me.Member.Name;
        }
        #endregion

        private void OnPropertyChanged(string property = null)
        {
            var pceh = PropertyChanged;
            if (pceh != null)
                pceh(this, new PropertyChangedEventArgs(property));
        }
        private void OnPropertyChanged(Expression<Func<object>> property)
        {
            OnPropertyChanged(GetPropertyExpr(property));
        }
        private void OnPropertyChanged(params Expression<Func<object>>[] property)
        {
            var pceh = PropertyChanged;
            if (pceh != null)
            {
                foreach (var prop in property)
                    pceh(this, new PropertyChangedEventArgs(GetPropertyExpr(prop)));
            }
        }
        #endregion

        #region SetPropertyChanged method group
        protected void SetPropertyChanged<T>(ref T member, T value, string property = null)
        {
            if (member.Equals(value))
                return;
            member = value;
            OnPropertyChanged(property);
        }
        protected void SetPropertyChanged<T>(ref T member, T value, Expression<Func<object>> property)
        {
            SetPropertyChanged(ref member, value, GetPropertyExpr(property));
        }
        protected void SetPropertyChanged<T>(ref T member, T value, params Expression<Func<object>>[] property)
        {
            if (member.Equals(value))
                return;
            member = value;
            var pceh = PropertyChanged;
            if (pceh != null)
            {
                foreach (var prop in property)
                    pceh(this, new PropertyChangedEventArgs(GetPropertyExpr(prop)));
            }
        }
        #endregion
        #endregion

        private string Status_ = String.Empty;
        private string Filename_ = String.Empty;
        private string ToolOutput_ = String.Empty;
        private bool IsDeviceConnected_ = false;
        private bool IsExpanded_ = false;

        // Button commands
        public DelegateCommand PickFolderCmd { get; private set; }
        public DelegateCommand FlashCmd { get; private set; }
        public DelegateCommand RedetectCmd { get; private set; }
        public DelegateCommand ExpandoCmd { get; private set; }

        // The status string at the top of the window
        public string Status
        {
            get { return Status_; }
            set { SetPropertyChanged(ref Status_, value, () => Status); }
        }
        // The currently selected file
        public string Filename
        {
            get { return Filename_; }
            set
            {
                Filename_ = value;
                FilenameChangeNotification();
            }
        }
        // The output of the current run of the tool
        public string ToolOutput { get { return ToolOutput_; } private set { SetPropertyChanged(ref ToolOutput_, value); } }
        public bool IsDeviceConnected { get { return IsDeviceConnected_; } private set { SetPropertyChanged(ref IsDeviceConnected_, value, () => IsDeviceConnected, () => ReadyForFlashing); } }
        // Is the Tool Detail pane expanded?
        public bool IsExpanded { get { return IsExpanded_; } private set { SetPropertyChanged(ref IsExpanded_, value, () => IsExpanded, () => ExpandoText); } }
        public string ExpandoText { get { return IsExpanded ? "Hide Details" : "Show Details"; } }

        private int Percent_ = 0;
        public int Percent { get { return Percent_; } set { SetPropertyChanged(ref Percent_, value); } }
        // Is the device ready for flashing, and a file is selected?
        public bool ReadyForFlashing { get { return IsFilenameValid && IsDeviceConnected; } }

        private void FilenameChangeNotification()
        {
            OnPropertyChanged(() => Filename, () => IsFilenameValid, () => ReadyForFlashing);
        }

        // Is the filename valid?
        // TODO:  Make this actually validate that the file is a good file for flashing
        // not just validate that it exists
        public bool IsFilenameValid
        {
            get { return !String.IsNullOrWhiteSpace(Filename) && System.IO.File.Exists(Filename); }
        }

        public MainWindow()
        {
            RedetectCmd = new DelegateCommand(RedetectDevice);
            PickFolderCmd = new DelegateCommand(PickFolder);
            FlashCmd = new DelegateCommand(Flash);
            ExpandoCmd = new DelegateCommand(() => IsExpanded = !IsExpanded);
            Status = "Detecting device. Please wait...";
            RedetectDevice();
            InitializeComponent();
            // You can pass the firmware filename on the command line
            if (!String.IsNullOrWhiteSpace(App.Arg) && System.IO.File.Exists(App.Arg))
                Filename = App.Arg;
        }

        void PickFolder()
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.CheckFileExists = true;
            ofd.CheckPathExists = true;
            ofd.DereferenceLinks = true;
            ofd.Multiselect = false;
            ofd.Title = "Select the Firmware Update file";
            bool? res = ofd.ShowDialog(this);
            if (res.HasValue && res.Value)
                Filename = ofd.FileName;
        }

        private static CmdLineWrapper CreateFirmwareProcess(params string[] args)
        {
            return CreateFirmwareProcess(args.AsEnumerable<string>());
        }

        private static CmdLineWrapper CreateFirmwareProcess(IEnumerable<string> args)
        {
            string app = App.FlashToolName;
            string extraArg = App.ExtraArgument;
            if (!String.IsNullOrWhiteSpace(extraArg))
                args = args.Prepend(extraArg);
            return new CmdLineWrapper(app, args);
        }

        private static string Quote(string str)
        {
            if (str.StartsWith("\""))
                return str;
            else
                return String.Format("\"{0}\"", str);
        }

        private void SetStatus(string info, int exitCode)
        {
            if (exitCode == 0)
                Status = String.Format("{0} completed successfully!", info);
            else
                Status = String.Format("{0} failed with error code {1}", info, exitCode);
        }

        void Flash()
        {
            if (!IsDeviceConnected)
            {
                Status = "Device not connected...";
            }
            else
            {
                flashStringState = 0;
                HitDevice("Flashing", CheckFlashString, () => { }, "-e", "-w", "-v", "-vid", App.vid, "-pid", App.pid, "-ix", Quote(Filename));
            }
        }

        private void RedetectDevice()
        {
            IsDeviceConnected = false;
            connectionStringState = 0;
            HitDevice("Detecting", CheckConnectionString, () => { }, "-ver", "-vid", App.vid, "-pid", App.pid);
        }

        /// <summary>
        /// Run the fw_update operation in a background task, reading in the stdout &amp;
        /// stderr streams, and call actions based on the results.
        /// </summary>
        /// <param name="operation">The description of the operation being run</param>
        /// <param name="handleLine">A function to process the line of text from the tool. Returns true/false if it's done, null if it should continue</param>
        /// <param name="failure">The action to run if the operation fails</param>
        /// <param name="args">The args to the tool to execute</param>
        private void HitDevice(string operation, Func<string, bool?> handleLine, Action failure, params string[] args)
        {
            Status = String.Format("{0} device...", operation);
            ToolOutput = "";
            // Launch the whole thing in a background task
            new Task(() =>
            {
                var tool = CreateFirmwareProcess(args);
                if (tool.Run())
                {
                    bool? result = null;
                    while (!result.HasValue)
                    {
                        var line = tool.StdOut.ReadLine();
                        ToolOutput += line + "\r\n";
                        result = handleLine(line);
                    }
                    tool.Proc.WaitForExit();
                    ToolOutput += tool.StdOut.ReadToEnd();
                    if (result.Value && tool.ExitCode == 0)
                        Status = String.Format("{0} completed successfully!", operation);
                    else
                        Status = String.Format("{0} failed with error code {1}", operation, tool.ExitCode);
                }
                else
                {
                    ToolOutput = "<--- an error occurred: make sure the tool is next to the application! --->";
                    Status = "An error occurred when trying to run the firmware update tool.";
                    failure();
                }
            }).Start();
        }

        // Maybe I should expose this stuff to the world?
        private string versionFWUpdate = null;
        private string versionBootLoader = null;

        private static readonly Regex firstLine_ver = new Regex(@"fw_update Version\: (\d+\.\d+\.\d+)", RegexOptions.IgnoreCase);
        private static readonly Regex bootLoader_ver = new Regex(@"U2IO BootLoader Version reading\: ((?:(?:DONE)|(?:FAILED))\.|(?:\d+\%))", RegexOptions.IgnoreCase);
        private static readonly Regex bootLoadVer_ver = new Regex(@"BootLoader Version\: (\d+\.\d+\.\d+)", RegexOptions.IgnoreCase);
        
        private int connectionStringState = 0;
        // validate the output from the version check handleLine
        private bool? CheckConnectionString(string stdout)
        {
            if (String.IsNullOrWhiteSpace(stdout))
                return null;
            stdout = stdout.Trim();
            switch (connectionStringState)
            {
                // State 0: no output yet seen
                // Output is either error or state 1
                case 0:
                    {
                        Match match = firstLine_ver.Match(stdout);
                        if (!match.Success)
                        {
                            ToolOutput += "fw_update version string not found\r\n";
                            return false;
                        }
                        if (match.Groups.Count != 2)
                        {
                            ToolOutput += "invalid return for fw regex\r\n";
                            return false;
                        }
                        versionFWUpdate = match.Groups[1].Value;
                        connectionStringState = 1;
                        return null;
                    }
                // State 1: reading the 'U2IO BootLoader" status line
                // Output is either error, state 1 (not yet seen "DONE") or state 2 (seen "DONE")
                case 1:
                    {
                        Match match = bootLoader_ver.Match(stdout);
                        if (!match.Success || match.Groups.Count != 2)
                            return false;
                        string doneFailedPercent = match.Groups[1].Value.ToString().ToLowerInvariant();
                        switch (doneFailedPercent)
                        {
                            case "done.":
                                connectionStringState = 2;
                                return null;
                            case "failed.":
                                return false;
                            default:
                                return null;
                        }
                    }
                // State 2: reading the BootLoader version string
                // Output is eithe error or success
                case 2:
                    {
                        Match match = bootLoadVer_ver.Match(stdout);
                        if (!match.Success || match.Groups.Count != 2)
                            return false;
                        versionBootLoader = match.Groups[1].Value;
                        IsDeviceConnected = true;
                        return true;
                    }
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private static readonly Regex u2io_flash = new Regex(@"U2IO flash ([a-z]+)\:\s+((?:DONE\.)|(?:FAILED\.)|(?:\d+\%))", RegexOptions.IgnoreCase);
        private int flashStringState = 0;
        // validate the output from the flash handleLine
        private bool? CheckFlashString(string stdout)
        {
            if (String.IsNullOrWhiteSpace(stdout))
                return null;
            stdout = stdout.Trim();
            if (flashStringState >= 0 && flashStringState <= 2)
            {
                var match = u2io_flash.Match(stdout);
                if (!match.Success || match.Groups.Count != 3)
                {
                    return false;
                }
                string op = match.Groups[1].Value.ToLowerInvariant();
                string state = match.Groups[2].Value.ToLowerInvariant();
                switch (flashStringState)
                {
                    // State 0: waiting for flash erasing to complete
                    // Output is error, state 0 (still waiting) or state 1 (programming)
                    case 0:
                        if (op != "erasing" || state == "failed.")
                            return false;
                        if (state == "done.")
                            flashStringState = 1;
                        return null;
                    // State 0: waiting for flash programming to complete
                    // Output is error, state 1 (still waiting) or state 2 (verifying)
                    case 1:
                        if (op != "programming" || state == "failed.")
                            return false;
                        if (state == "done.")
                            flashStringState = 2;
                        else
                            Percent = Int32.Parse(state.TrimEnd('%'));
                        return null;
                    // State 0: waiting for flash verification to complete
                    // Output is error, state 2 (still waiting) or state 3 (RESET)
                    case 2:
                        if (op != "verifying" || state == "failed.")
                            return false;
                        if (state == "done.")
                            flashStringState = 3;
                        else
                            Percent = Int32.Parse(state.TrimEnd('%'));
                        return null;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }

            switch (flashStringState)
            {
                // State 3: Finished veryfing: waiting for "RESET Device"
                // Output state: erro or 4 (looking for success report)
                case 3:
                    if (stdout.ToLowerInvariant() != "reset device")
                    {
                        ToolOutput += "Post-flash reset appears to have failed\r\n";
                        return false;
                    }
                    flashStringState = 4;
                    return null;
                case 4:
                    if (stdout.ToLowerInvariant() != "operation successfully completed.")
                    {
                        ToolOutput += "Post-flash reset appears to have failed\r\n";
                        return false;
                    }
                    return true;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
    }
}
